// Copyright (c) 2020 IoTeX Foundation
// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
// This source code is governed by Apache License 2.0 that can be found in the LICENSE file.

package action

import (
	"math/big"
	"slices"

	"google.golang.org/protobuf/proto"

	"github.com/iotexproject/go-pkgs/hash"
	"github.com/iotexproject/iotex-address/address"
	"github.com/iotexproject/iotex-proto/golang/iotextypes"
	"github.com/pkg/errors"

	"github.com/iotexproject/iotex-core/v2/pkg/log"
)

type (
	// Topics are data items of a transaction, such as send/recipient address
	Topics []hash.Hash256

	// Receipt represents the result of a contract
	Receipt struct {
		Status             uint64
		BlockHeight        uint64
		ActionHash         hash.Hash256
		GasConsumed        uint64
		BlobGasUsed        uint64
		BlobGasPrice       *big.Int
		ContractAddress    string
		TxIndex            uint32
		EffectiveGasPrice  *big.Int
		logs               []*Log
		transactionLogs    []*TransactionLog
		executionRevertMsg string
		// Output is used to store the Output of contract execution IN MEMORY only, not serialized to DB
		// it is used for the caller to get the Output of a contract call
		Output []byte
	}

	// Log stores an evm contract event
	Log struct {
		Address            string
		Topics             Topics
		Data               []byte
		BlockHeight        uint64
		ActionHash         hash.Hash256
		Index, TxIndex     uint32
		NotFixTopicCopyBug bool
	}

	// TransactionLog stores a transaction event
	TransactionLog struct {
		Type      iotextypes.TransactionLogType
		Amount    *big.Int
		Sender    string
		Recipient string
	}
)

// ConvertToReceiptPb converts a Receipt to protobuf's Receipt
func (receipt *Receipt) ConvertToReceiptPb() *iotextypes.Receipt {
	r := &iotextypes.Receipt{}
	r.Status = receipt.Status
	r.BlkHeight = receipt.BlockHeight
	r.ActHash = receipt.ActionHash[:]
	r.GasConsumed = receipt.GasConsumed
	r.ContractAddress = receipt.ContractAddress
	r.TxIndex = receipt.TxIndex
	r.Logs = []*iotextypes.Log{}
	for _, l := range receipt.logs {
		r.Logs = append(r.Logs, l.ConvertToLogPb())
	}
	if receipt.executionRevertMsg != "" {
		r.ExecutionRevertMsg = receipt.executionRevertMsg
	}
	r.BlobGasUsed = receipt.BlobGasUsed
	if receipt.BlobGasPrice != nil {
		r.BlobGasPrice = receipt.BlobGasPrice.String()
	}
	if receipt.EffectiveGasPrice != nil {
		r.EffectiveGasPrice = receipt.EffectiveGasPrice.String()
	}
	return r
}

// ConvertFromReceiptPb converts a protobuf's Receipt to Receipt
func (receipt *Receipt) ConvertFromReceiptPb(pbReceipt *iotextypes.Receipt) {
	receipt.Status = pbReceipt.GetStatus()
	receipt.BlockHeight = pbReceipt.GetBlkHeight()
	copy(receipt.ActionHash[:], pbReceipt.GetActHash())
	receipt.GasConsumed = pbReceipt.GetGasConsumed()
	receipt.ContractAddress = pbReceipt.GetContractAddress()
	receipt.TxIndex = pbReceipt.GetTxIndex()
	logs := pbReceipt.GetLogs()
	receipt.logs = make([]*Log, len(logs))
	for i, log := range logs {
		receipt.logs[i] = &Log{}
		receipt.logs[i].ConvertFromLogPb(log)
	}
	receipt.executionRevertMsg = pbReceipt.GetExecutionRevertMsg()
	receipt.BlobGasUsed = pbReceipt.GetBlobGasUsed()
	if pbReceipt.GetBlobGasPrice() != "" {
		receipt.BlobGasPrice = new(big.Int)
		receipt.BlobGasPrice.SetString(pbReceipt.GetBlobGasPrice(), 10)
	}
	if pbReceipt.GetEffectiveGasPrice() != "" {
		receipt.EffectiveGasPrice = new(big.Int)
		receipt.EffectiveGasPrice.SetString(pbReceipt.GetEffectiveGasPrice(), 10)
	}
}

// Serialize returns a serialized byte stream for the Receipt
func (receipt *Receipt) Serialize() ([]byte, error) {
	return proto.Marshal(receipt.ConvertToReceiptPb())
}

// Deserialize parse the byte stream into Receipt
func (receipt *Receipt) Deserialize(buf []byte) error {
	pbReceipt := &iotextypes.Receipt{}
	if err := proto.Unmarshal(buf, pbReceipt); err != nil {
		return err
	}
	receipt.ConvertFromReceiptPb(pbReceipt)
	return nil
}

// Hash returns the hash of receipt
func (receipt *Receipt) Hash() hash.Hash256 {
	data, err := receipt.Serialize()
	if err != nil {
		log.L().Panic("Error when serializing a receipt")
	}
	return hash.Hash256b(data)
}

// Logs returns the list of logs stored in receipt
func (receipt *Receipt) Logs() []*Log {
	return receipt.logs
}

// AddLogs add log to receipt and filter out nil log.
func (receipt *Receipt) AddLogs(logs ...*Log) *Receipt {
	for _, l := range logs {
		if l != nil {
			receipt.logs = append(receipt.logs, l)
		}
	}
	return receipt
}

// TransactionLogs returns the list of transaction logs stored in receipt
func (receipt *Receipt) TransactionLogs() []*TransactionLog {
	return receipt.transactionLogs
}

// AddTransactionLogs add transaction logs to receipt and filter out nil log.
func (receipt *Receipt) AddTransactionLogs(logs ...*TransactionLog) *Receipt {
	for _, l := range logs {
		if l != nil {
			receipt.transactionLogs = append(receipt.transactionLogs, l)
		}
	}
	return receipt
}

// ExecutionRevertMsg returns the list of execution revert error logs stored in receipt.
func (receipt *Receipt) ExecutionRevertMsg() string {
	return receipt.executionRevertMsg
}

// SetExecutionRevertMsg sets executionerrorlogs to receipt.
func (receipt *Receipt) SetExecutionRevertMsg(revertReason string) *Receipt {
	if receipt.executionRevertMsg == "" && revertReason != "" {
		receipt.executionRevertMsg = revertReason
	}
	return receipt
}

func (receipt *Receipt) PriorityFee() *big.Int {
	txLogs := receipt.TransactionLogs()
	if id := slices.IndexFunc(txLogs, func(t *TransactionLog) bool { return t.Type == iotextypes.TransactionLogType_PRIORITY_FEE }); id != -1 {
		return txLogs[id].Amount
	}
	return nil
}

// UpdateIndex updates the index of receipt and logs, and returns the next log index
func (receipt *Receipt) UpdateIndex(txIndex, logIndex uint32) uint32 {
	receipt.TxIndex = txIndex
	for _, l := range receipt.logs {
		l.TxIndex = txIndex
		l.Index = logIndex
		logIndex++
	}
	return logIndex
}

// TransferLogs converts transaction logs to logs with given account contract address and starting log index
func (receipt *Receipt) TransferLogs(accountContractAddr string, logIndex uint32) ([]*Log, error) {
	logs := make([]*Log, 0, len(receipt.transactionLogs))
	for _, tlog := range receipt.transactionLogs {
		log, err := tlog.convertToLog()
		if err != nil {
			return nil, errors.Wrapf(err, "failed to convert transaction log to log: %s", tlog.Type)
		}
		log.Address = accountContractAddr
		log.BlockHeight = receipt.BlockHeight
		log.ActionHash = receipt.ActionHash
		log.Index = logIndex
		log.TxIndex = receipt.TxIndex
		logs = append(logs, log)
		logIndex++
	}
	return logs, nil
}

// ConvertToLogPb converts a Log to protobuf's Log
func (log *Log) ConvertToLogPb() *iotextypes.Log {
	l := &iotextypes.Log{}
	l.ContractAddress = log.Address
	l.Topics = [][]byte{}
	for _, topic := range log.Topics {
		if log.NotFixTopicCopyBug {
			l.Topics = append(l.Topics, log.Topics[len(log.Topics)-1][:])
		} else {
			data := make([]byte, len(topic))
			copy(data, topic[:])
			l.Topics = append(l.Topics, data)
		}
	}
	l.Data = log.Data
	l.BlkHeight = log.BlockHeight
	l.ActHash = log.ActionHash[:]
	l.Index = log.Index
	l.TxIndex = log.TxIndex
	return l
}

// ConvertFromLogPb converts a protobuf's LogPb to Log
func (log *Log) ConvertFromLogPb(pbLog *iotextypes.Log) {
	log.Address = pbLog.GetContractAddress()
	pbLogs := pbLog.GetTopics()
	log.Topics = make([]hash.Hash256, len(pbLogs))
	for i, topic := range pbLogs {
		copy(log.Topics[i][:], topic)
	}
	log.Data = pbLog.GetData()
	log.BlockHeight = pbLog.GetBlkHeight()
	copy(log.ActionHash[:], pbLog.GetActHash())
	log.Index = pbLog.GetIndex()
	log.TxIndex = pbLog.GetTxIndex()
}

// Serialize returns a serialized byte stream for the Log
func (log *Log) Serialize() ([]byte, error) {
	return proto.Marshal(log.ConvertToLogPb())
}

// Deserialize parse the byte stream into Log
func (log *Log) Deserialize(buf []byte) error {
	pbLog := &iotextypes.Log{}
	if err := proto.Unmarshal(buf, pbLog); err != nil {
		return err
	}
	log.ConvertFromLogPb(pbLog)
	return nil
}

func convertSpecialAddress(addr string) string {
	switch addr {
	case address.StakingBucketPoolAddr:
		return address.StakingProtocolAddr
	case address.RewardingPoolAddr:
		return address.RewardingProtocol
	case "":
		return address.ZeroAddress
	}
	return addr
}

func (tlog *TransactionLog) convertToLog() (*Log, error) {
	sender := convertSpecialAddress(tlog.Sender)
	recipient := convertSpecialAddress(tlog.Recipient)
	from, err := address.FromString(sender)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to convert sender address from string: %s", sender)
	}
	to, err := address.FromString(recipient)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to convert recipient address from string: %s", recipient)
	}
	topics, data, err := PackAccountTransferEvent(from, to, tlog.Amount, uint8(tlog.Type))
	if err != nil {
		return nil, errors.Wrapf(err, "failed to pack account transfer event: %s", tlog.Type)
	}
	return &Log{
		Topics: topics,
		Data:   data,
	}, nil
}
